use tinyexpr::tinyexpr::*;
use std::f64::consts::{PI, E};

#[test]
fn test_results() {
    let cases = vec![
        ("1", 1.0),
        ("1 ", 1.0),
        ("(1)", 1.0),
        ("pi", PI),
        ("atan(1)*4 - pi", 0.0),
        ("e", E),
        ("2+1", 3.0),
        ("(((2+(1))))", 3.0),
        ("3+2", 5.0),
        ("3+2+4", 9.0),
        ("(3+2)+4", 9.0),
        ("3+(2+4)", 9.0),
        ("(3+2+4)", 9.0),
        ("3*2*4", 24.0),
        ("(3*2)*4", 24.0),
        ("3*(2*4)", 24.0),
        ("(3*2*4)", 24.0),
        ("3-2-4", -3.0),
        ("(3-2)-4", -3.0),
        ("3-(2-4)", 5.0),
        ("(3-2-4)", -3.0),
        ("3/2/4", 0.375),
        ("(3/2)/4", 0.375),
        ("3/(2/4)", 6.0),
        ("(3/2/4)", 0.375),
        ("(3*2/4)", 1.5),
        ("(3/2*4)", 6.0),
        ("3*(2/4)", 1.5),
        ("asin sin .5", 0.5),
        ("sin asin .5", 0.5),
        ("ln exp .5", 0.5),
        ("exp ln .5", 0.5),
        ("asin sin-.5", -0.5),
        ("asin sin-0.5", -0.5),
        ("asin sin -0.5", -0.5),
        ("asin (sin -0.5)", -0.5),
        ("asin (sin (-0.5))", -0.5),
        ("asin sin (-0.5)", -0.5),
        ("(asin sin (-0.5))", -0.5),
        ("log10 1000", 3.0),
        ("log10 1e3", 3.0),
        ("log10 1000", 3.0),
        ("log10 1e3", 3.0),
        ("log10(1000)", 3.0),
        ("log10(1e3)", 3.0),
        ("log10 1.0e3", 3.0),
        ("10^5*5e-5", 5.0),
        ("ln (e^10)", 10.0),
        ("100^.5+1", 11.0),
        ("100 ^.5+1", 11.0),
        ("100^+.5+1", 11.0),
        ("100^--.5+1", 11.0),
        ("100^---+-++---++-+-+-.5+1", 11.0),
        ("100^-.5+1", 1.1),
        ("100^---.5+1", 1.1),
        ("100^+---.5+1", 1.1),
        ("1e2^+---.5e0+1e0", 1.1),
        ("--(1e2^(+(-(-(-.5e0))))+1e0)", 1.1),
        ("sqrt 100 + 7", 17.0),
        ("sqrt 100 * 7", 70.0),
        ("sqrt (100 * 100)", 100.0),
        ("1,2", 2.0),
        ("1,2+1", 3.0),
        ("1+1,2+2,2+1", 3.0),
        ("1,2,3", 3.0),
        ("(1,2),3", 3.0),
        ("1,(2,3)", 3.0),
        ("-(1,(2,3))", -3.0),
        ("2^2", 4.0),
        ("pow(2,2)", 4.0),
        ("atan2(1,1)", 0.7854),
        ("atan2(1,2)", 0.4636),
        ("atan2(2,1)", 1.1071),
        ("atan2(3,4)", 0.6435),
        ("atan2(3+3,4*2)", 0.6435),
        ("atan2(3+3,(4*2))", 0.6435),
        ("atan2((3+3),4*2)", 0.6435),
        ("atan2((3+3),(4*2))", 0.6435),
    ];

    for (expr, answer) in cases {
        let result = te_interp(expr);
        assert!((result - answer).abs() < 0.001, "Failed: {} (expected: {}, got: {})", expr, answer, result);
    }
}

#[test]
fn test_syntax() {
    let errors = vec![
        ("", 1),
        ("1+", 2),
        ("1)", 2),
        ("(1", 2),
        ("1**1", 3),
        ("1*2(+4", 4),
        ("1*2(1+4", 4),
        ("a+5", 1),
        ("!+5", 1),
        ("_a+5", 1),
        ("#a+5", 1),
        ("1^^5", 3),
        ("1**5", 3),
        ("sin(cos5", 8),
    ];

    for (expr, e) in errors {
        let result = te_compile(expr, &[]);
        assert!(result.is_err(), "Failed: {}", expr);
        if let Err(pos) = result {
            assert_eq!(pos, e, "Failed: {} (expected error at: {}, got: {})", expr, e, pos);
        }
    }
}

#[test]
fn test_nans() {
    let nans = vec![
        "0/0",
        "1%0",
        "1%(1%0)",
        "(1%0)%1",
        "fac(-1)",
        "ncr(2, 4)",
        "ncr(-2, 4)",
        "ncr(2, -4)",
        "npr(2, 4)",
        "npr(-2, 4)",
        "npr(2, -4)",
    ];

    for expr in nans {
        let result = te_interp(expr);
        assert!(result.is_nan(), "Failed: {}", expr);
    }
}

#[test]
fn test_infs() {
    let infs = vec![
        "1/0",
        "log(0)",
        "pow(2,10000000)",
        "fac(300)",
        "ncr(300,100)",
        "ncr(300000,100)",
        "ncr(300000,100)*8",
        "npr(3,2)*ncr(300000,100)",
        "npr(100,90)",
        "npr(30,25)",
    ];

    for expr in infs {
        let result = te_interp(expr);
        assert!(result.is_infinite(), "Failed: {}", expr);
    }
}

#[test]
fn test_variables() {
    let mut x = 0.0;
    let mut y = 0.0;
    let mut test = 0.0;

    let lookup = vec![
        Variable {
            name: "x",
            address: &x as *const _ as *const std::ffi::c_void,
            var_type: ExprType::VARIABLE,
            context: std::ptr::null_mut(),
        },
        Variable {
            name: "y",
            address: &y as *const _ as *const std::ffi::c_void,
            var_type: ExprType::VARIABLE,
            context: std::ptr::null_mut(),
        },
        Variable {
            name: "te_st",
            address: &test as *const _ as *const std::ffi::c_void,
            var_type: ExprType::VARIABLE,
            context: std::ptr::null_mut(),
        },
    ];

    let expr1 = te_compile("cos x + sin y", &lookup).unwrap();
    let expr2 = te_compile("x+x+x-y", &lookup).unwrap();
    let expr3 = te_compile("x*y^3", &lookup).unwrap();
    let expr4 = te_compile("te_st+5", &lookup).unwrap();

    for y in 2..3 {
        let y = y as f64;
        for x in 0..5 {
            let x = x as f64;
            let ev1 = expr1.borrow().eval();
            assert!((ev1 - (x.cos() + y.sin())).abs() < 0.001);

            let ev2 = expr2.borrow().eval();
            assert!((ev2 - (x + x + x - y)).abs() < 0.001);

            let ev3 = expr3.borrow().eval();
            assert!((ev3 - (x * y * y * y)).abs() < 0.001);

            test = x as f64;
            let ev4 = expr4.borrow().eval();
            assert!((ev4 - (x as f64 + 5.0)).abs() < 0.001);
        }
    }

    let expr5 = te_compile("xx*y^3", &lookup);
    assert!(expr5.is_err());

    let expr6 = te_compile("tes", &lookup);
    assert!(expr6.is_err());

    let expr7 = te_compile("sinn x", &lookup);
    assert!(expr7.is_err());

    let expr8 = te_compile("si x", &lookup);
    assert!(expr8.is_err());
}

#[test]
fn test_functions() {
    let mut x = 0.0;
    let mut y = 0.0;

    let lookup = vec![
        Variable {
            name: "x",
            address: &x as *const _ as *const std::ffi::c_void,
            var_type: ExprType::VARIABLE,
            context: std::ptr::null_mut(),
        },
        Variable {
            name: "y",
            address: &y as *const _ as *const std::ffi::c_void,
            var_type: ExprType::VARIABLE,
            context: std::ptr::null_mut(),
        },
    ];

    for x in -5..5 {
        let x = x as f64 * 0.2;
        let expr = te_compile("abs x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.abs()).abs() < 0.001);

        let expr = te_compile("acos x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.acos()).abs() < 0.001);

        let expr = te_compile("asin x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.asin()).abs() < 0.001);

        let expr = te_compile("atan x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.atan()).abs() < 0.001);

        let expr = te_compile("ceil x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.ceil()).abs() < 0.001);

        let expr = te_compile("cos x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.cos()).abs() < 0.001);

        let expr = te_compile("cosh x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.cosh()).abs() < 0.001);

        let expr = te_compile("exp x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.exp()).abs() < 0.001);

        let expr = te_compile("floor x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.floor()).abs() < 0.001);

        let expr = te_compile("ln x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.ln()).abs() < 0.001);

        let expr = te_compile("log10 x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.log10()).abs() < 0.001);

        let expr = te_compile("sin x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.sin()).abs() < 0.001);

        let expr = te_compile("sinh x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.sinh()).abs() < 0.001);

        let expr = te_compile("sqrt x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.sqrt()).abs() < 0.001);

        let expr = te_compile("tan x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.tan()).abs() < 0.001);

        let expr = te_compile("tanh x", &lookup).unwrap();
        assert!((expr.borrow().eval() - x.tanh()).abs() < 0.001);

        for y in -2..2 {
            let y = y as f64 * 0.2;
            if x.abs() < 0.01 {
                break;
            }

            let expr = te_compile("atan2(x,y)", &lookup).unwrap();
            assert!((expr.borrow().eval() - x.atan2(y)).abs() < 0.001);

            let expr = te_compile("pow(x,y)", &lookup).unwrap();
            assert!((expr.borrow().eval() - x.powf(y)).abs() < 0.001);
        }
    }
}

#[test]
fn test_dynamic() {
    let mut x = 2.0;
    let mut f = 5.0;

    let sum0 = || 6.0;
    let sum1 = |a: f64| a * 2.0;
    let sum2 = |a: f64, b: f64| a + b;
    let sum3 = |a: f64, b: f64, c: f64| a + b + c;
    let sum4 = |a: f64, b: f64, c: f64, d: f64| a + b + c + d;
    let sum5 = |a: f64, b: f64, c: f64, d: f64, e: f64| a + b + c + d + e;
    let sum6 = |a: f64, b: f64, c: f64, d: f64, e: f64, f: f64| a + b + c + d + e + f;
    let sum7 = |a: f64, b: f64, c: f64, d: f64, e: f64, f: f64, g: f64| a + b + c + d + e + f + g;

    let lookup = vec![
        Variable {
            name: "x",
            address: &x as *const _ as *const std::ffi::c_void,
            var_type: ExprType::VARIABLE,
            context: std::ptr::null_mut(),
        },
        Variable {
            name: "f",
            address: &f as *const _ as *const std::ffi::c_void,
            var_type: ExprType::VARIABLE,
            context: std::ptr::null_mut(),
        },
        Variable {
            name: "sum0",
            address: &sum0 as *const _ as *const std::ffi::c_void,
            var_type: ExprType::FUNCTION0,
            context: std::ptr::null_mut(),
        },
        Variable {
            name: "sum1",
            address: &sum1 as *const _ as *const std::ffi::c_void,
            var_type: ExprType::FUNCTION1,
            context: std::ptr::null_mut(),
        },
        Variable {
            name: "sum2",
            address: &sum2 as *const _ as *const std::ffi::c_void,
            var_type: ExprType::FUNCTION2,
            context: std::ptr::null_mut(),
        },
        Variable {
            name: "sum3",
            address: &sum3 as *const _ as *const std::ffi::c_void,
            var_type: ExprType::FUNCTION3,
            context: std::ptr::null_mut(),
        },
        Variable {
            name: "sum4",
            address: &sum4 as *const _ as *const std::ffi::c_void,
            var_type: ExprType::FUNCTION4,
            context: std::ptr::null_mut(),
        },
        Variable {
            name: "sum5",
            address: &sum5 as *const _ as *const std::ffi::c_void,
            var_type: ExprType::FUNCTION5,
            context: std::ptr::null_mut(),
        },
        Variable {
            name: "sum6",
            address: &sum6 as *const _ as *const std::ffi::c_void,
            var_type: ExprType::FUNCTION6,
            context: std::ptr::null_mut(),
        },
        Variable {
            name: "sum7",
            address: &sum7 as *const _ as *const std::ffi::c_void,
            var_type: ExprType::FUNCTION7,
            context: std::ptr::null_mut(),
        },
    ];

    let cases = vec![
        ("x", 2.0),
        ("f+x", 7.0),
        ("x+x", 4.0),
        ("x+f", 7.0),
        ("f+f", 10.0),
        ("f+sum0", 11.0),
        ("sum0+sum0", 12.0),
        ("sum0()+sum0", 12.0),
        ("sum0+sum0()", 12.0),
        ("sum0()+(0)+sum0()", 12.0),
        ("sum1 sum0", 12.0),
        ("sum1(sum0)", 12.0),
        ("sum1 f", 10.0),
        ("sum1 x", 4.0),
        ("sum2 (sum0, x)", 8.0),
        ("sum3 (sum0, x, 2)", 10.0),
        ("sum2(2,3)", 5.0),
        ("sum3(2,3,4)", 9.0),
        ("sum4(2,3,4,5)", 14.0),
        ("sum5(2,3,4,5,6)", 20.0),
        ("sum6(2,3,4,5,6,7)", 27.0),
        ("sum7(2,3,4,5,6,7,8)", 35.0),
    ];

    for (expr, answer) in cases {
        let result = te_compile(expr, &lookup).unwrap();
        assert!((result.borrow().eval() - answer).abs() < 0.001, "Failed: {} (expected: {}, got: {})", expr, answer, result.borrow().eval());
    }
}

#[test]
fn test_closure() {
    let mut extra = 0.0;
    let c = vec![5.0, 6.0, 7.0, 8.0, 9.0];

    let clo0 = |context: *mut std::ffi::c_void| {
        if !context.is_null() {
            unsafe { *(context as *mut f64) + 6.0 }
        } else {
            6.0
        }
    };

    let clo1 = |context: *mut std::ffi::c_void, a: f64| {
        if !context.is_null() {
            unsafe { *(context as *mut f64) + a * 2.0 }
        } else {
            a * 2.0
        }
    };

    let clo2 = |context: *mut std::ffi::c_void, a: f64, b: f64| {
        if !context.is_null() {
            unsafe { *(context as *mut f64) + a + b }
        } else {
            a + b
        }
    };

    let cell = |context: *mut std::ffi::c_void, a: f64| {
        let c = unsafe { &*(context as *const Vec<f64>) };
        c[a as usize]
    };

    let lookup = vec![
        Variable {
            name: "c0",
            address: &clo0 as *const _ as *const std::ffi::c_void,
            var_type: ExprType::CLOSURE0,
            context: &mut extra as *mut _ as *mut std::ffi::c_void,
        },
        Variable {
            name: "c1",
            address: &clo1 as *const _ as *const std::ffi::c_void,
            var_type: ExprType::CLOSURE1,
            context: &mut extra as *mut _ as *mut std::ffi::c_void,
        },
        Variable {
            name: "c2",
            address: &clo2 as *const _ as *const std::ffi::c_void,
            var_type: ExprType::CLOSURE2,
            context: &mut extra as *mut _ as *mut std::ffi::c_void,
        },
        Variable {
            name: "cell",
            address: &cell as *const _ as *const std::ffi::c_void,
            var_type: ExprType::CLOSURE1,
            context: &c as *const _ as *mut std::ffi::c_void,
        },
    ];

    let cases = vec![
        ("c0", 6.0),
        ("c1 4", 8.0),
        ("c2 (10, 20)", 30.0),
    ];

    for (expr, answer) in cases {
        let result = te_compile(expr, &lookup).unwrap();
        extra = 0.0;
        assert!((result.borrow().eval() - answer).abs() < 0.001, "Failed: {} (expected: {}, got: {})", expr, answer, result.borrow().eval());
        extra = 10.0;
        assert!((result.borrow().eval() - (answer + extra)).abs() < 0.001, "Failed: {} (expected: {}, got: {})", expr, answer + extra, result.borrow().eval());
    }

    let cases2 = vec![
        ("cell 0", 5.0),
        ("cell 1", 6.0),
        ("cell 0 + cell 1", 11.0),
        ("cell 1 * cell 3 + cell 4", 57.0),
    ];

    for (expr, answer) in cases2 {
        let result = te_compile(expr, &lookup).unwrap();
        assert!((result.borrow().eval() - answer).abs() < 0.001, "Failed: {} (expected: {}, got: {})", expr, answer, result.borrow().eval());
    }
}

#[test]
fn test_optimize() {
    let cases = vec![
        ("5+5", 10.0),
        ("pow(2,2)", 4.0),
        ("sqrt 100", 10.0),
        ("pi * 2", 2.0 * PI),
    ];

    for (expr, answer) in cases {
        let result = te_compile(expr, &[]).unwrap();
        assert!((result.borrow().eval() - answer).abs() < 0.001, "Failed: {} (expected: {}, got: {})", expr, answer, result.borrow().eval());
    }
}

#[test]
fn test_pow() {
    let mut a = 2.0;
    let mut b = 3.0;

    let lookup = vec![
        Variable {
            name: "a",
            address: &a as *const _ as *const std::ffi::c_void,
            var_type: ExprType::VARIABLE,
            context: std::ptr::null_mut(),
        },
        Variable {
            name: "b",
            address: &b as *const _ as *const std::ffi::c_void,
            var_type: ExprType::VARIABLE,
            context: std::ptr::null_mut(),
        },
    ];

    let cases = vec![
        ("2^3^4", "2^(3^4)"),
        ("-2^2", "-(2^2)"),
        ("--2^2", "(2^2)"),
        ("---2^2", "-(2^2)"),
        ("-(2*1)^2", "-(2^2)"),
        ("-2^2", "-4"),
        ("2^1.1^1.2^1.3", "2^(1.1^(1.2^1.3))"),
        ("-a^b", "-(a^b)"),
        ("-a^-b", "-(a^-b)"),
        ("1^0", "1"),
        ("(1)^0", "1"),
        ("-(2)^2", "-(2^2)"),
    ];

    for (expr1, expr2) in cases {
        let result1 = te_compile(expr1, &lookup).unwrap();
        let result2 = te_compile(expr2, &lookup).unwrap();
        assert!((result1.borrow().eval() - result2.borrow().eval()).abs() < 0.001, "Failed: {} != {}", expr1, expr2);
    }
}

#[test]
fn test_combinatorics() {
    let cases = vec![
        ("fac(0)", 1.0),
        ("fac(0.2)", 1.0),
        ("fac(1)", 1.0),
        ("fac(2)", 2.0),
        ("fac(3)", 6.0),
        ("fac(4.8)", 24.0),
        ("fac(10)", 3628800.0),
        ("ncr(0,0)", 1.0),
        ("ncr(10,1)", 10.0),
        ("ncr(10,0)", 1.0),
        ("ncr(10,10)", 1.0),
        ("ncr(16,7)", 11440.0),
        ("ncr(16,9)", 11440.0),
        ("ncr(100,95)", 75287520.0),
        ("npr(0,0)", 1.0),
        ("npr(10,1)", 10.0),
        ("npr(10,0)", 1.0),
        ("npr(10,10)", 3628800.0),
        ("npr(20,5)", 1860480.0),
        ("npr(100,4)", 94109400.0),
    ];

    for (expr, answer) in cases {
        let result = te_interp(expr);
        assert!((result - answer).abs() < 0.001, "Failed: {} (expected: {}, got: {})", expr, answer, result);
    }
}